---
title: Python Typing 101
description: A gentle, example‑driven introduction to static type hints in Python.
---

import CodeSnippet from '@site/src/sandbox/CodeSnippet'

# Python Typing 101

*A beginner‑friendly guide to adding type hints in Python.*

**Note:** This tutorial assumes you understand some basic Python syntax, but are new to programming with type hints. To learn more about Python, see the [Python Tutorial](https://docs.python.org/3/tutorial/) and [Getting Started Guide](https://www.python.org/about/gettingstarted/)

## 1. What is a Type?

A type is a classification that defines what operations can be performed on a piece of data, what values it can hold, and how it behaves in memory. Types are fundamental to programming because they help ensure that operations on data make sense.

For example:
- An `int` (integer) type can be added, subtracted, or multiplied
- A `str` (string) type can be concatenated or split
- A `list` type can be indexed, sliced, or iterated over

**Note:** These are just examples of common operations for each data type. Python's built-in types support many more operations that are not listed here.

Understanding types helps you predict how your code will behave and avoid runtime errors from trying to perform operations that don't make sense, such as dividing a string by a number.

## 2. What is a Type Hint in Python?

A type hint in Python is a way to indicate the expected data type of a variable, function parameter, or return value. It's a hint to other developers (and to tools like type checkers and IDEs) about what type of data should be used with a particular piece of code.

Type hints are **not enforced at runtime by Python itself,** but they can be used by third-party tools (like Pyrefly) to catch type-related errors before your code runs. They also serve as documentation, making it easier for others to understand how to use your code.
Here's an example of a simple function with type hints:

```
def greet(name: str) -> None:
    print(f"Hello, {name}!")
```

In this example:
- `name: str` indicates that the `name` parameter should be a string.
- `-> None` specifies that the function doesn't return any value (similar to `void` in other languages).

## 3. Why Bother with Type Hints?

Python is a dynamically typed language, which means you can write code without declaring types. However, this can lead to bugs or ambiguity in your code.

_TL;DR_
* Catch bugs **before** running the code.
* Improve editor autocomplete & refactors.
* Turn your code into living documentation.


<CodeSnippet
  sampleFilename="why_hints.py"
  codeSample={`# Without hints – is "times" a str, int, or list?
def repeat(text, times):
    return text * times

# With hints – intent is crystal clear.
def repeat(text: str, times: int) -> str:
    return text * times
`}
/>

In this example:
- The first function lacks type hints, making it unclear what types `text` and `times` should be. The `*` operator works differently depending on types (string repetition, list repetition, or multiplication).
- The second function uses type hints to clearly indicate that `text` should be a string, `times` should be an integer, and the function returns a string.
- This clarity helps prevent bugs like accidentally passing a string for `times` or using the function incorrectly.


### Can you spot the bug?

```
class Rectangle:
    width: int
    height: int

    def __init__(self, width: int, height: int) -> None:
        self.width = width
        self.height = height

rect = Rectangle(width=100, height=50)

area = rect.width * rect.hieght

print(area)
```

In this example:
- The bug is a typo in `rect.hieght` (should be `rect.height`).
- Without type hints, Python would only report this error at runtime when it tries to access the non-existent attribute.
- With type hints and a tool like Pyrefly, this error would be caught before running the code because the `Rectangle` class has defined attributes `width` and `height`, but not `hieght`.

**Spelling is hard!** Let's add the `dataclass` decorator to our class definition. This will generate a constructor for us, and also add a few other useful methods.

<CodeSnippet
  sampleFilename="misspelled.py"
  codeSample={`#Pyrefly will catch this spelling error before you run the code
from dataclasses import dataclass

@dataclass
class Rectangle:
    width: int
    height: int

rect = Rectangle(width=100, height=50)

area = rect.width * rect.hieght
`}
/>

In this dataclass example:
- The `@dataclass` decorator automatically generates methods like `__init__`, `__repr__`, and `__eq__` based on the class attributes.
- Type hints are used to define the class attributes (`width: int`, `height: int`).
- The same spelling error exists (`rect.hieght`), but tools like Pyrefly can catch this before runtime because the dataclass clearly defines which attributes exist.
- This demonstrates how type hints combined with dataclasses provide both convenience and better error detection.


## 4. Primitive Types

Since Python 3.9 you can use all the [primitive types](https://docs.python.org/3/library/stdtypes.html) directly as annotations.

<CodeSnippet
  sampleFilename="primitives_types.py"
  codeSample={`age: int = 30
height: float = 1.85
name: str = "Tyler Typer"
is_admin: bool = False
`}
/>

In this primitive types example:
- Each variable is annotated with its expected type (`int`, `float`, `str`, `bool`).
- The values assigned match their declared types.
- These annotations help document the code and allow type checkers to verify that operations on these variables are valid for their types.
- For example, a type checker would flag an error if you tried `age + name` since adding an integer and string isn't a valid operation.

You can also specify a parameter as optional by using `Optional` type, or now with the `| None` syntax.

<CodeSnippet
  sampleFilename="primitive2_types.py"
  codeSample={`# Optional typing example

from typing import Optional

middle_name: Optional[str] = None        # classic
nickname: str | None = None              # 3.10+ shorthand
`}
/>

In this Optional type example:
- Both variables can either be a string or `None`.
- `Optional[str]` is the traditional syntax (pre-Python 3.10).
- `str | None` is the newer union syntax introduced in Python 3.10.
- These annotations tell type checkers that the variable might be `None`, so they can warn you if you try to perform string operations without checking for `None` first.

## 5. Collections

### Syntax	Examples
- List of numbers	`list[int]	scores: list[int] = [98, 87, 91]`
- Tuple of two floats	`tuple[float, float]	point: tuple[float, float] = (3.0, 4.0)`
- Dict of str -> int	`dict[str, int]	inventory: dict[str, int] = {"apples": 5}`
- Set of strings	`set[str]	authors: set[str] = {"Bob", "Eve"}`

Since Python 3.9 you can subscript built‑ins directly—no need for `from typing import List`.

## 6. Functions
<CodeSnippet
  sampleFilename="basic_function_types.py"
  codeSample={`# Simple function
def add(a: int, b: int) -> int:
    return a + b
  `}
/>

In this basic function example:
- Both parameters `a` and `b` are annotated as integers.
- The function is annotated to return an integer (`-> int`).
- This tells type checkers that the function should only be called with integers and that the return value should only be used in contexts where an integer is expected.

Default values keep their annotation:

<CodeSnippet
  sampleFilename="default_value_types.py"
  codeSample={`# Function with default value


def greet(name: str, polite: bool = True) -> str:
    return "Hello!" if polite else f"Yo {name}"
    `}
/>

In this function with default values:
- The `name` parameter must be a string.
- The `polite` parameter is a boolean with a default value of `True`.
- The function returns a string.
- Even though `polite` has a default value, it still has a type annotation to ensure that if it's explicitly provided, it must be a boolean.

Variable‑length arguments:

<CodeSnippet
  sampleFilename="variable_length_types.py"
  codeSample={`# Variable length functions
from collections.abc import Callable

Logger = Callable[[str], None]

def debug(*msgs: str, log: Logger | None) -> None:
    for m in msgs:
      if log is not None:
        log(m)
      else:
        print(m)
  `}
/>

In this variable-length arguments example:
- `Logger` is defined as a type alias for a callable that takes a string and returns nothing (`None`).
- `*msgs: str` indicates that the function accepts any number of string arguments.
- `log: Logger | None` means the `log` parameter can be either a Logger function or `None`.
- The function is annotated to return `None`.
- This demonstrates how to type complex function signatures with variable arguments and function parameters.

## 7. Get Type Hint Signals Directly in Your Editor

You can download the [Pyrefly extension for VSCode](https://marketplace.visualstudio.com/items?itemName=meta.pyrefly) to get type hint signals directly in your IDE.

Next, install [Pyrefly](../installation/) and check some code:
```
# Fast, zero‑config
pip install pyrefly

pyrefly check ./my_sample.py

# Check whole directories
pyrefly check app/ tests/
```

Create a `pyrefly.toml` file to configure your project. Instructions [here](../configuration).
